import RBush, { type BBox } from 'rbush'
import type { LngLat } from '@/geom/great-circle'
import { lngLatToSegDist } from '@/geom/euclidean'
import type { AuxGraphic, AuxGraphicGeometry } from './base'

interface IndexItem {
  minX: number
  minY: number
  maxX: number
  maxY: number
  graphic: AuxGraphic
}

type PointItem = IndexItem

function pointItem(ag: AuxGraphic, coordinates: number[]): PointItem {
  const x = coordinates[0]
  const y = coordinates[1]
  return {
    minX: x,
    maxX: x,
    minY: y,
    maxY: y,
    graphic: ag
  }
}

interface SegmentItem extends IndexItem {
  start: LngLat
  end: LngLat
}

function segmentItem(ag: AuxGraphic, start: LngLat, end: LngLat): SegmentItem {
  return {
    start,
    end,
    graphic: ag,
    minX: Math.min(start.lng, end.lng),
    maxX: Math.max(start.lng, end.lng),
    minY: Math.min(start.lat, end.lat),
    maxY: Math.max(start.lat, end.lat)
  }
}

function segmentItems(ag: AuxGraphic, coord: number[][]): SegmentItem[] {
  const items: SegmentItem[] = []
  let prePt: LngLat | null = null
  for (let i = 0; i < coord.length; i++) {
    const pt = { lng: coord[i][0], lat: coord[i][1] }
    if (!prePt) {
      prePt = pt
      continue
    }
    items.push(segmentItem(ag, prePt, pt))
  }
  return items
}

export class AuxGraphicIndex {
  private pIdx = new RBush<PointItem>(16)
  private sIdx = new RBush<SegmentItem>(16)
  private agItems: Record<string, IndexItem[]> = {}

  public add(ag: AuxGraphic, geometries: AuxGraphicGeometry[]) {
    const items: IndexItem[] = []
    for (const g of geometries) {
      if (g.type === 'Point') {
        const it = pointItem(ag, g.coordinates)
        items.push(it)
      } else if (g.type === 'LineString') {
        const its = segmentItems(ag, g.coordinates)
        items.push(...its)
      } else if (g.type === 'Polygon') {
        for (const coord of g.coordinates) {
          const its = segmentItems(ag, coord)
          items.push(...its)
        }
      }
    }
    for (const it of items) {
      if ('start' in it) {
        this.sIdx.insert(it as SegmentItem)
      } else {
        this.pIdx.insert(it)
      }
    }
    this.agItems[ag.id] = items
  }

  public removeById(id: string) {
    const items = this.agItems[id]
    if (!items) {
      return
    }
    for (const it of items) {
      if ('start' in it) {
        this.sIdx.remove(it as SegmentItem)
      } else {
        this.pIdx.remove(it)
      }
    }
    delete this.agItems[id]
  }

  public remove(ag: AuxGraphic) {
    this.removeById(ag.id)
  }

  public searchOne(lng: number, lat: number, radius: number): AuxGraphic | null {
    const bBox = {
      minX: lng - radius,
      minY: lat - radius,
      maxX: lng + radius,
      maxY: lng + radius
    }
    const ret = this.searchOnePoint(bBox)
    if (ret) {
      return ret
    }
    return this.searchOneSegment(bBox, { lng, lat }, radius)
  }

  private searchOnePoint(bBox: BBox): AuxGraphic | null {
    const it = this.pIdx.search(bBox)
    if (!it || it.length <= 0) {
      return null
    }
    return it[0].graphic
  }

  private searchOneSegment(bBox: BBox, pt: LngLat, radius: number): AuxGraphic | null {
    const its = this.sIdx.search(bBox)
    if (!its) {
      return null
    }
    for (const it of its) {
      const d1 = lngLatToSegDist(pt, it.start, it.end)
      if (d1 <= radius) {
        return it.graphic
      }
    }
    return null
  }
}
